package com.richie696.catalina.config;

import com.richie696.catalina.*;
import com.richie696.catalina.connector.Connector;
import com.richie696.connect.CatalinaExecutor;
import com.richie696.connect.Service;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.utils.IOUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;

public final class ConfigParser {

    private static final ConfigParser INSTANCE = new ConfigParser();

    private ConfigParser() {
    }

    public static ConfigParser getInstance() {
        return INSTANCE;
    }

    public Service read(String fileName) throws LifecycleException {
        Service service = new Service();
        Document rootDocument = parseDocument(fileName);
        parseService(service, rootDocument);
        parseExecutor(service, rootDocument);
        parseEngine(service, rootDocument);
        parseConnector(service, rootDocument);
        parseWarPackage(service);
        return service;
    }

    private Document parseDocument(String fileName) throws LifecycleException {
        SAXReader reader = new SAXReader();
        Document rootDocument;
        try {
            InputStream is = ConfigParser.class.getResourceAsStream(fileName);
            rootDocument = reader.read(is);
        } catch (DocumentException e) {
            throw new LifecycleException(e);
        }
        return rootDocument;
    }

    private void parseService(Service service, Document rootDocument) {
        Element serviceElem = (Element) rootDocument.selectSingleNode("//Service");
        service.setName(serviceElem.attributeValue("name"));
    }

    private void parseExecutor(Service service, Document rootDocument) {
        List<Node> executors = rootDocument.selectNodes("//Executor");
        if (executors.isEmpty()) {
            return;
        }
        for (Node node : executors) {
            Element element = (Element) node;
            CatalinaExecutor executor = new CatalinaExecutor();
            String name = element.attributeValue("name");
            String namePrefix = element.attributeValue("namePrefix");
            int maximumQueueSize = Integer.parseInt(element.attributeValue("maximumQueueSize"));
            int maxThreads = Integer.parseInt(element.attributeValue("maximumQueueSize"));
            int corePoolSize = Integer.parseInt(element.attributeValue("corePoolSize"));
            long keepAliveTime = Long.parseLong(element.attributeValue("keepAliveTime"));
            executor.init(name, namePrefix, corePoolSize, maximumQueueSize, keepAliveTime, maxThreads).build();
            service.addExecutor(name, executor);
        }
    }

    private void parseConnector(Service service, Document rootDocument) throws LifecycleException {
        List<Node> nodes = rootDocument.selectNodes("//Connector");
        if (nodes.isEmpty()) {
            throw new LifecycleException("Connector 连接器配置错误，至少需要一个连接器。");
        }
        for (Node node : nodes) {
            Element element = (Element) node;
            Connector connector = new Connector();
            connector.setPort(Integer.parseInt(element.attributeValue("port")));
            connector.setExecutor(element.attributeValue("executor"));
            connector.setExecutorBean(service.getExecutor(connector.getExecutor()));
            connector.setProtocol(element.attributeValue("protocol"));
            connector.setEngine(service.getEngine());
            service.addConnector(connector.getPort(), connector);
        }
    }

    private void parseEngine(Service service, Document rootDocument) throws LifecycleException {
        List<Node> nodes = rootDocument.selectNodes("//Engine");
        if (nodes.isEmpty()) {
            throw new LifecycleException("Engine 主机配置错误，至少需要一个主机配置。");
        }
        Engine engine = new MappedEngine();
        for (Node node : nodes) {
            Element element = (Element) node;
            engine.setName(element.attributeValue("name"));
            engine.setDefaultHost(element.attributeValue("defaultHost"));
            element.elements().forEach(el -> engine.addHost(el.attributeValue("name"), el.attributeValue("appBase")));
        }
        service.setEngine(engine);
    }


    private void parseWarPackage(Service service) {
        for (Host host : service.getEngine().getHosts()) {
            File[] warFiles = host.getAppBaseFile().listFiles();
            if (Objects.isNull(warFiles)) {
                continue;
            }
            for (File warFile : warFiles) {
                if (warFile.isDirectory()) {
                    continue;
                }
                if (!warFile.getName().endsWith("war")) {
                    continue;
                }
                String warName = warFile.getName().split("\\.")[0];
                File[] files = host.getAppBaseFile().listFiles(
                        new DirectoryFileFilter(warName)
                );
                if (Objects.nonNull(files) && files.length > 0) {
                    for (File file : files) {
                        //noinspection ResultOfMethodCallIgnored
                        file.delete();
                    }
                }
                Wrapper wrapper = new StandardWrapper();
                wrapper.setName(warName);
                host.addWrapper(wrapper);
                doUnzipWar(host, warFile, warName);
            }
        }

    }

    private void doUnzipWar(Host host, File warFile, String warName) {
        String base = warFile.getParent();
        base = base + File.separator + warName + File.separator;
        try (InputStream fis = new FileInputStream(warFile);
             InputStream bis = new BufferedInputStream(fis);
             ArchiveInputStream ais = new ZipArchiveInputStream(bis)) {
            ArchiveEntry entry;
            while (Objects.nonNull(entry = ais.getNextEntry())) {
                if (!ais.canReadEntryData(entry)) {
                    continue;
                }
                String name = base + entry.getName();
                File target = new File(name);
                if (entry.isDirectory()) {
                    if (!target.isDirectory() && !target.mkdirs()) {
                        if (!target.mkdirs()) {
                            throw new RuntimeException("当前目录没有权限执行写入操作。 target = " + target.getPath());
                        }
                    }
                } else {
                    File parent = target.getParentFile();
                    if (!parent.isDirectory() && !parent.mkdirs()) {
                        throw new IOException("failed to create directory " + parent);
                    }
                    try (OutputStream o = Files.newOutputStream(target.toPath())) {
                        IOUtils.copy(ais, o);
                    }
                }
            }
            loadWarContent(host, base, warName);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void loadWarContent(Host host, String base, String warName) throws Exception {
        Wrapper wrapper = host.getWrapper(warName);
        wrapper.setWarAbstractPath(base);
        SAXReader reader = new SAXReader();
        InputStream is = Files.newInputStream(Paths.get(base + "WEB-INF" + File.separator + "web.xml"));
        Element xmlDoc = reader.read(is).getRootElement();

        List<Element> nodes = xmlDoc.elements("servlet");
        for (Element servletEl : nodes) {
            String servletName = servletEl.element("servlet-name").getTextTrim();
            String servletClass = servletEl.element("servlet-class").getTextTrim();
            ServletMapping mapping = wrapper.getServletMapping(servletName);
            if (Objects.isNull(mapping)) {
                mapping = new ServletMapping();
                mapping.setServletName(servletName);
                mapping.setServletClass(servletClass);
                wrapper.addServletMapping(servletName, mapping);
            }
            List<Node> mappingNodes = xmlDoc.selectNodes("/web-app/servlet-mapping[servlet-name='" + servletName + "']");
            for (Node mappingNode : mappingNodes) {
                Element mappingEl = (Element) mappingNode;
                mapping.addUrlPattern(mappingEl.selectSingleNode("url-pattern").getStringValue().trim());
            }
        }
    }

    static class DirectoryFileFilter implements FileFilter {

        private final String name;
        public DirectoryFileFilter(String name) {
            this.name = name;
        }

        @Override
        public boolean accept(File pathname) {
            return pathname.isDirectory() && pathname.getName().equals(name);
        }
    }

}
